{%- set clazz_name = clazz['name'] -%}
{%- set snake_name = camel_to_snake(clazz_name) -%}
{%- set instance_methods = clazz['methods'] | selectattr('is_vararg', 'equalto', false) | selectattr('is_static', 'equalto', false) | list -%}
{%- set static_methods = clazz['methods'] | selectattr('is_vararg', 'equalto', false) | selectattr('is_static', 'equalto', true) | list-%}
{%- set vararg_methods = clazz['methods'] | selectattr('is_vararg', 'equalto', true) | list -%}
{%- set members = clazz['members'] -%}
{%- set class_id = clazz_name + '::__class_id' %}
{%- macro get_const(method) %}
{{-'_const' if method['is_const'] else ''-}}
{%- endmacro %}
{%- if has_vararg_method(clazz) %}
#include "register/builtin_classes/callable_vararg_method.hpp"
{%- endif %}
#include "register/builtin_classes/register_builtin_classes.h"
#include "quickjs/quickjs.h"
#include "quickjs/env.h"
#include "utils/func_utils.hpp"
#include "utils/str_helper.hpp"
#include "utils/quickjs_helper.hpp"
#include "utils/variant_helper.hpp"
#include <godot_cpp/variant/{{camel_to_snake(clazz_name)}}.hpp>

using namespace godot;

static void {{snake_name}}_class_finalizer(JSRuntime *rt, JSValue val) {
	JSClassID class_id = classes["{{clazz_name}}"];
	{{clazz_name}} *{{snake_name}} = static_cast<{{clazz_name}} *>(JS_GetOpaque(val, class_id));
	if ({{snake_name}})
		memfree({{snake_name}});
}

static JSClassDef {{snake_name}}_class_def = {
	"{{clazz_name}}",
	.finalizer = {{snake_name}}_class_finalizer
};

static JSValue {{snake_name}}_class_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv) {
	JSClassID class_id = classes["{{clazz_name}}"];
	JSValue obj = JS_NewObjectClass(ctx, class_id);
	if (JS_IsException(obj))
		return obj;

	{{clazz_name}} *{{snake_name}}_class = nullptr;
	{%- set constructs = clazz['constructors'] %}
	{% for ctor in constructs %}
	if (argc == {{ctor['arguments'] | length}} {{variant_type_cond(ctor['arguments'])}}) {
		{%- for arg in ctor['arguments'] %}
		{{arg['type']}} v{{loop.index-1}} = VariantAdapter(argv[{{loop.index-1}}]);
		{%- endfor %}
		{{snake_name}}_class = memnew({{clazz_name}}({{put_args(ctor['arguments'])}}));
	}
	{% endfor %}

	if (!{{snake_name}}_class) {
		JS_FreeValue(ctx, obj);
		return JS_EXCEPTION;
	}

	JS_SetOpaque(obj, {{snake_name}}_class);
	return obj;
}
{%- for method in instance_methods %}
static JSValue {{snake_name}}_class_{{method['name']}}(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
		{%- if method['return_type'] %}
	return call_builtin{{get_const(method)}}_method_ret(&{{clazz_name}}::{{method['name']}}, ctx, this_val, argc, argv);
		{%- else %}
    call_builtin{{get_const(method)}}_method_no_ret(&{{clazz_name}}::{{method['name']}}, ctx, this_val, argc, argv);
	return JS_UNDEFINED;
		{%- endif %}
};
{%- endfor %}
{%- for method in  static_methods %}
static JSValue {{snake_name}}_class_{{method['name']}}(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
		{%- if method['return_type'] %}
	return call_builtin_static_method_ret(&{{clazz_name}}::{{method['name']}}, ctx, this_val, argc, argv);
		{%- else %}
    call_builtin_static_method_method_no_ret(&{{clazz_name}}::{{method['name']}}, ctx, this_val, argc, argv);
	return JS_UNDEFINED;
		{%- endif %}
};
{% endfor %}

{%- for method in vararg_methods %}
static JSValue {{snake_name}}_class_{{method['name']}}(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
    {%- if method['return_type'] and method['arguments'] %}
	return call_builtin_free_opaque_vararg_method_ret<{{clazz_name}}>(&js_{{method['name']}}, ctx, this_val, argc, argv);
	{%- elif method['return_type'] %}
	return call_builtin_free_opaque_no_fixed_vararg_method_ret_impl<{{clazz_name}}>(&js_{{method['name']}}, ctx, this_val, argc, argv);
	{%- elif method['arguments'] %}
	return call_builtin_free_opaque_vararg_method_no_ret<{{clazz_name}}>(&js_{{method['name']}}, ctx, this_val, argc, argv);
    {%- else %}
	return call_builtin_free_opaque_no_fixed_vararg_method_no_ret_impl<{{clazz_name}}>(&js_{{method['name']}}, ctx, this_val, argc, argv);
    {%- endif %}
}
{%- endfor %}

{%- if members %}
{%- for member in members %}
static JSValue {{snake_name}}_class_get_{{member['name']}}(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
	{{clazz_name}} &val = *reinterpret_cast<{{clazz_name}} *>(JS_GetOpaque(this_val, classes["{{clazz_name}}"]));
	return VariantAdapter(val.{{member['name']}});
}
static JSValue {{snake_name}}_class_set_{{member['name']}}(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
	{{clazz_name}} &val = *reinterpret_cast<{{clazz_name}} *>(JS_GetOpaque(this_val, classes["{{clazz_name}}"]));
	val.{{member['name']}} = VariantAdapter(*argv);
	return JS_UNDEFINED;
}
{%- endfor %}
{%- endif %}

{%- if instance_methods or vararg_methods %}
static const JSCFunctionListEntry {{snake_name}}_class_proto_funcs[] = {
	{%- for method in instance_methods + vararg_methods %}
	JS_CFUNC_DEF("{{ method['name'] }}", {{method['arguments'] | length}}, &{{snake_name}}_class_{{method['name']}}),
	{%- endfor %}
};
{%- endif %}

{%- if static_methods %}
static const JSCFunctionListEntry {{snake_name}}_class_static_funcs[] = {
	{%- for method in static_methods %}
	JS_CFUNC_DEF("{{ method['name'] }}", {{method['arguments'] | length}}, &{{snake_name}}_class_{{method['name']}}),
	{%- endfor %}
};
{%- endif %}

{%- macro mgetset(gs,method) %}
	{%-  if gs in clazz['methods'] | map(attribute='name') -%}
	JS_NewCFunction(ctx, {{snake_name}}_class_{{gs}}, "{{gs}}", 0)
	{%- elif gs in clazz['methods'] | map(attribute='name') -%}
	JS_NewCFunction(ctx, {{snake_name}}_class_{{gs}}, "{{gs}}", 1)
	{%- else -%}
	JS_UNDEFINED
	{%- endif -%}
{%- endmacro %}

{%- if members %}
void define_{{snake_name}}_property(JSContext *ctx, JSValue obj) {
	{%- for member in members %}
    JS_DefinePropertyGetSet(
        ctx,
        obj,
        JS_NewAtom(ctx, "{{member['name']}}"),
        JS_NewCFunction(ctx, {{snake_name}}_class_get_{{member['name']}}, "get_{{member['name']}}", 0),
        JS_NewCFunction(ctx, {{snake_name}}_class_set_{{member['name']}}, "set_{{member['name']}}", 1),
		JS_PROP_GETSET
    );
	{%- endfor %}
}
{%- endif %}

{%- if clazz['constants'] %}
void define_{{snake_name}}_constants(JSContext *ctx, JSValue ctor) {
	{%- for constant in clazz['constants'] %}
	JS_DefinePropertyValueStr(ctx, ctor, "{{constant['name']}}", VariantAdapter({{constant['value']}}), JS_PROP_ENUMERABLE);
	{%- endfor %}
}
{%- endif %}

static int js_{{snake_name}}_class_init(JSContext *ctx) {
	classes["{{clazz_name}}"] = 0;
	classes["{{clazz_name}}"] = JS_NewClassID(&classes["{{clazz_name}}"]);
	JSClassID class_id = classes["{{clazz_name}}"];

	class_id_list.insert(class_id);
	JS_NewClass(JS_GetRuntime(ctx), class_id, &{{snake_name}}_class_def);

	JSValue proto = JS_NewObject(ctx);
	{%- if clazz['inherits'] %}
	JSValue base_class = JS_GetClassProto(ctx, {{clazz['inherits']}}::__class_id);
	JS_SetPrototype(ctx, proto, base_class);
	{%- endif %}
	JS_SetClassProto(ctx, class_id, proto);

	{%- if members %}
	define_{{snake_name}}_property(ctx, proto);
	{%- endif %}
	{%- if instance_methods or vararg_methods %}
	JS_SetPropertyFunctionList(ctx, proto, {{snake_name}}_class_proto_funcs, _countof({{snake_name}}_class_proto_funcs));
	{%- endif %}

	JSValue ctor = JS_NewCFunction2(ctx, {{snake_name}}_class_constructor, "{{clazz_name}}", 0, JS_CFUNC_constructor, 0);
	JS_SetConstructor(ctx, ctor, proto);
	{%- if static_methods %}
	JS_SetPropertyFunctionList(ctx, ctor, {{snake_name}}_class_static_funcs, _countof({{snake_name}}_class_static_funcs));
	{%- endif %}
	{%- if clazz['constants'] %}
	define_{{snake_name}}_constants(ctx, ctor);
	{%- endif %}

	JSValue global = JS_GetGlobalObject(ctx);
	JS_SetPropertyStr(ctx, global, "{{clazz_name}}", ctor);

	return 0;
}

void js_init_{{ snake_name }}_module(JSContext *ctx) {
	js_{{snake_name}}_class_init(ctx);
}

void register_{{ snake_name }}() {
	js_init_{{ snake_name }}_module(ctx);
}